;以下常量定义部分。内核的大部分内容都应当固定 
core_code_seg_sel     equ  0x38    ;内核代码段选择子
core_data_seg_sel     equ  0x30    ;内核数据段选择子 
sys_routine_seg_sel   equ  0x28    ;系统公共例程代码段的选择子 
video_ram_seg_sel     equ  0x20    ;视频显示缓冲区的段选择子
core_stack_seg_sel    equ  0x18    ;内核堆栈段选择子
mem_0_4_gb_seg_sel    equ  0x08    ;整个0-4GB内存的段的选择子

;分配内存的函数
;输入:ECX是希望分配的字节数
;输出:ECX是起始线性地址
allocate_memory:
    ;保存ds寄存器的值
    push ds
    ;保存eax寄存器的值
    push eax
    ;保存ebx寄存器的值
    push ebx
    ;把内核数据段的选择子赋值给eax寄存器
    mov eax,core_data_seg_sel
    ;把eax寄存器的值赋值给ds寄存器
    mov ds,eax
    ;ram_alloc是下次分配内存时的起始地址，把它赋值给eax寄存器
    mov eax,[ram_alloc]
    ;eax的值加上ecx的值，得到需要的内存大小的地址
    add eax,ecx
    ;把下次分配内存的起始地址，赋值给ecx寄存器，ECX返回的时候是分配的起始地址
    mov ecx,[ram_alloc]
    ;把eax的值赋值给ebx作为备份
    mov ebx,eax
    ;把ebx的值和0xfffffffc进行与操作，得到4的倍数的地址
    and ebx,0xfffffffc
    ;把ebx的值加上4，得到4的倍数的地址
    add ebx,4
    ;检测原先的地址是否是4的倍数
    test eax,0x00000003
    ;如果不是4的倍数，就把ebx的值赋值给eax
    cmovnz eax,ebx
    ;把eax的值赋值给ram_alloc，下次分配内存时从eax开始分配
    mov [ram_alloc],eax
    ;弹出ebx寄存器的值
    pop ebx
    ;弹出eax寄存器的值
    pop eax
    ;弹出ds寄存器的值
    pop ds
    retf

;在GDT内安装一个新的描述符
set_up_gdt_descriptor:
    ;保存eax寄存器的值
    push eax
    ;保存ebx寄存器的值
    push ebx
    ;保存edx寄存器的值
    push edx
    ;保存ds寄存器的值
    push ds
    ;保存es寄存器的值
    push es
    ;把内核代码段的选择子赋值给ebx寄存器
    mov ebx,core_data_seg_sel
    ;把ebx寄存器的值赋值给ds寄存器，让ds指向内核代码段
    mov ds,ebx

    ;获取当前GDT的信息
    ; sgdt 是汇编指令，用于获取全局描述符表（GDT）的信息。
    ; [pgdt] 是内存地址，该指令会将 GDT 的界限值和基地址存储到 [pgdt] 指定的内存位置。
    ; 界限值存储在低 2 字节，基地址存储在接下来的 4 字节。
    sgdt [pgdt]
    
    ;把可以访问整个4GB内存的段选择子赋值给ebx寄存器
    ;把ebx寄存器的值赋值给es寄存器，让es指向可以访问整个4GB内存的段
    mov ebx,mem_0_4_gb_seg_sel
    ;把ebx的值赋值给es寄存器，这样es就指向可以访问整个4GB内存的段
    mov es,ebx

    ;从pgdt中获取当前GDT的界限值，并用movzx指令扩展到32后存入ebx
    movzx ebx,word [pgdt]
    ;将界限值+1，得到GDT表的实际大小
    inc bx
    ;将ebx的值加上pgdt+2，得到GDT表末尾的下一个地址
    ;为什么是末尾地址？因为新的段描述符需要添加到GDT的末尾
    add ebx,[pgdt+2]
    ;eax寄存器包含了段描述符的低4字节，将其存入es:ebx
    mov [es:ebx],eax
    ;edx寄存器包含了段描述符的高4字节，将其存入es:ebx+4
    mov [es:ebx+4],edx 
    ;将GDT的界限值+8，因为新添加了一个描述符
    add word [pgdt],8
    ;将新的GDT加载到GDTR寄存器
    lgdt [pgdt]
    ;把GDT的界限值赋值给ax寄存器
    mov ax,[pgdt]
    ;将dx寄存器清零，为后面的除法运算做准备
    xor dx,dx
    ;将除数设置为8，因为每个描述符占8字节
    mov bx,8
    ;dx:ax除以bx，商存在ax寄存器，余数存在dx寄存器
    div bx
    ;将描述符数量复制到cx寄存器
    mov cx,ax
    ;将描述符数量左移3位，得到描述符的字节数
    shl cx,3

    pop es
    pop ds

    pop edx
    pop ebx
    pop eax

    retf

;构造存储器和系统的段描述符
;输入:
;eax:段的基地址
;ebx:段的界限
;ecx:段的属性
;输出:
;eax:段描述符的低4字节
;edx:段描述符的高4字节
make_seg_descriptor:
    ;赋值段的基地址到edx寄存器
    mov edx,eax
    ;eax左移16位，将段基址的低16位移动到高16位
    shl eax,16
    ;将段的界限赋值给ax寄存器
    ;此时，段描述符的低32位就构造完成了
    or ax,bx
    ;保留段描述符的高16位，清除低16位
    and edx,0xffff0000
    ;EDX循环左移8位，让基地址的23-16位移动到正确的位置
    rol edx,8
    ;将基址23-16位位合并到EAX中
    or eax,edx
    ;清除BX寄存器
    xor bx,bx
    ;通过或操作间接清空 EDX 寄存器中除了已处理的基址位外的部分
    or edx,ebx
    ;将ECX中的段属性合并到EDX中
    or edx,ecx
    ;返回
    retf

SECTION core_data vstart=0
    pgdt        dw 0
                dd 0
    ram_alloc   dd 0x00100000
             ;符号地址检索表
         salt:
         salt_1           db  '@PrintString'
                     times 256-($-salt_1) db 0
                          dd  put_string
                          dw  sys_routine_seg_sel

         salt_2           db  '@ReadDiskData'
                     times 256-($-salt_2) db 0
                          dd  read_hard_disk_0
                          dw  sys_routine_seg_sel

         salt_3           db  '@PrintDwordAsHexString'
                     times 256-($-salt_3) db 0
                          dd  put_hex_dword
                          dw  sys_routine_seg_sel

         salt_4           db  '@TerminateProgram'
                     times 256-($-salt_4) db 0
                          dd  return_point
                          dw  core_code_seg_sel

         salt_item_len   equ $-salt_4
         salt_items      equ ($-salt)/salt_item_len

         message_1        db  '  If you seen this message,that means we '
                          db  'are now in protect mode,and the system '
                          db  'core is loaded,and the video display '
                          db  'routine works perfectly.',0x0d,0x0a,0

         message_5        db  '  Loading user program...',0
         
         do_status        db  'Done.',0x0d,0x0a,0
         
         message_6        db  0x0d,0x0a,0x0d,0x0a,0x0d,0x0a
                          db  '  User program terminated,control returned.',0

         bin_hex          db '0123456789ABCDEF'
                                            ;put_hex_dword子过程用的查找表
         core_buf   times 2048 db 0         ;内核用的缓冲区

         esp_pointer      dd 0              ;内核用来临时保存自己的栈指针     

         cpu_brnd0        db 0x0d,0x0a,'  ',0
         cpu_brand  times 49 db 0
         cpu_brnd1        db 0x0d,0x0a,0x0d,0x0a,0


;输入：esi寄存器指向起始逻辑扇区号
;输出：ax指向的用户程序头部的选择子
SECTION core_code vstart=0
load_relocate_program:
    ;将ebx寄存器中的值压栈
    push ebx
    ;将ecx寄存器中的值压栈
    push ecx
    ;将edx寄存器中的值压栈
    push edx
    ;将esi寄存器中的值压栈
    push esi
    ;将edi寄存器中的值压栈
    push edi
    ;将ds寄存器中的值压栈
    push ds
    ;将es寄存器中的值压栈
    push es
    ;将核心数据段选择子赋值给eax寄存器
    mov eax,core_data_seg_sel
    ;将eax寄存器中的值赋值给ds寄存器，ds目前指向核心数据段
    mov ds,eax
    ;将esi寄存器中的值（用户程序起始逻辑扇区号）赋值给eax寄存器
    mov eax,esi
    ;把内核程序的内存赋值给ebx寄存器，因为要把用户程序加载到内核程序内存中
    mov ebx,core_buf
    ;调用read_hard_disk_0子过程，读取用户程序的头部
    call sys_routine_seg_sel:read_hard_disk_0
    ;将用户程序的程序尺寸赋值给eax寄存器
    mov eax,[core_buf]
    ;把eax寄存器中的值（用户程序的程序尺寸）赋值给ebx寄存器
    mov ebx,eax
    ;把用户程序大小和512对齐
    and ebx,0xfffffe00
    ;把用户程序和512内存对齐之后，再把512添加到用户程序的尺寸中
    add ebx,512
    ;检测用户程序的大小是否是512的倍数
    test eax,0x000001ff
    ;不是，就把对齐之后的用户程序赋值给eax寄存器
    cmovnz eax,ebx
    ;把eax寄存器中的值（用户程序的程序尺寸）赋值给ecx寄存器（为了给用户程序分配内存）
    mov ecx,eax
    ;调用allocate_memory子过程，分配内存
    call sys_routine_seg_sel:allocate_memory
    ;把ecx的值（申请到的内存首地址）赋值给ebx寄存器
    mov ebx,ecx
    ;把ebx寄存器中的值（申请到的内存首地址）压栈
    push ebx
    ;将edx寄存器清空
    xor edx,edx
    ;把512赋值给ecx寄存器
    mov ecx,512
    ;把用户程序的程序尺寸除以512，得到用户程序的逻辑扇区数
    div ecx
    ;把商（用户程序的逻辑扇区数）赋值给ecx寄存器
    mov ecx,eax
    ;把0-4GB内存的段的选择子赋值给eax寄存器
    mov eax,mem_0_4_gb_seg_sel
    ;让ds寄存器指向0-4GB内存的段
    mov ds,eax
    ;把用户程序的起始逻辑扇区号赋值给eax寄存器
    mov eax,esi
.b1:
    ;调用read_hard_disk_0子过程，读取用户程序的逻辑扇区
    call sys_routine_seg_sel:read_hard_disk_0
    inc eax
    ;循环读，直到读完用户程序的所有逻辑扇区
    loop .b1

    ;建立程序头部段描述符
    ;恢复程序装载的首地址
    pop edi
    ;把程序头部起始的线性地址赋值给eax寄存器
    mov eax,edi
    ;获取段界限值
    mov ebx,[edi+0x04]
    ;界限值-1
    dec ebx
    ;设置段属性
    mov ecx,0x00409200
    ;创建段描述符
    call sys_routine_seg_sel:make_seg_descriptor
    ;将段描述符添加到GDT
    call sys_routine_seg_sel:set_up_gdt_descriptor
    ;保存返回的段选择子
    mov [edi+0x04],cx

    ;建立程序代码段描述符
    mov eax,edi
    add eax,[edi+0x0c]
    mov ebx,[edi+0x10]
    dec ebx
    mov ecx,0x00409800
    call sys_routine_seg_sel:make_seg_descriptor
    call sys_routine_seg_sel:set_up_gdt_descriptor
    mov [edi+0x0c],cx

    ;建立程序数据段描述符
    mov eax,edi
    add eax,[edi+0x14]
    mov ebx,[edi+0x18]
    dec ebx
    mov ecx,0x00409200
    call sys_routine_seg_sel:make_seg_descriptor
    call sys_routine_seg_sel:set_up_gdt_descriptor
    mov [edi+0x14],cx

    ;建立程序栈段描述符
    mov eax,edi
    add eax,[edi+0x1c]
    mov ebx,[edi+0x20]
    dec ebx
    mov ecx,0x00409200
    call sys_routine_seg_sel:make_seg_descriptor
    call sys_routine_seg_sel:set_up_gdt_descriptor
    mov [edi+0x1c],cx

    ;重定位SALT
    ;把用户程序的系统公用例程段选择子赋值给eax寄存器
    mov eax,[edi+0x04]
    ;把eax的值赋值给es寄存器（现在es指向用户程序的系统公用例程段）
    mov es,eax
    ;把核心代码段选择子赋值给eax寄存器
    mov eax,core_data_seg_sel
    ;把eax的值赋值给ds寄存器（现在ds指向核心代码段）
    mov ds,eax
    ;把方向标志位df清空
    cld 
    ;把用户程序的SALT项数赋值给ecx寄存器
    mov ecx,[es:0x24]
    ;给edi赋值0x28（用户程序内的SALT位于头部内0x28字节）
    mov edi,0x28
.b2:
    ;把ecx寄存器中的值（SALT项数）压栈
    push ecx
    ;把edi寄存器中的值（用户程序内的SALT位于头部内0x28字节）压栈
    push edi
    ;把内核程序的SALT项数赋值给ecx寄存器
    mov ecx,salt_items
    ;把内核程序的SALT起始地址赋值给esi寄存器
    mov esi,salt

.b3:
    push edi
    push esi
    push ecx
    ;后面比较字符的时候要连续比较64个双字，一共256字节
    mov ecx,64
    ;比较ESI指向的内存地址（内核程序的SALT）和EDI指向的内存地址（用户程序的SALT）
    repe cmpsd
    ;如果不为0（说明用户程序的SALT和内核程序的SALT不同），就跳转到.b4
    jnz .b4
    ;esi指向内核程序的SALT，若匹配，esi恰好指向其后的地址数据
    mov eax,[esi]
    ;将字符串改写成偏移地址
    mov [es:edi-256],eax
    ;移动4个字节，指向段选择子
    mov ax,[esi+4]
    ;把用户程序的字符串改写成段选择子
    mov [es:edi-252],ax
.b4:
    
    pop ecx
    pop esi
    add esi,salt_item_len
    pop edi
    loop .b3

    pop edi
    add edi,256
    pop ecx
    loop .b2

    mov ax,[es:0x04]

    pop es
    pop ds

    pop edi
    pop esi
    pop edx
    pop ecx
    pop ebx
    ret


start:
    mov ecx,core_data_seg_sel
    mov ds,ecx
    mov es,ecx
    mov ebx,message_1
    call sys_routine_seg_sel:put_string

    ;显示处理器品牌信息 
    mov eax,0x80000002
    cpuid
    mov [cpu_brand + 0x00],eax
    mov [cpu_brand + 0x04],ebx
    mov [cpu_brand + 0x08],ecx
    mov [cpu_brand + 0x0c],edx
      
    mov eax,0x80000003
    cpuid
    mov [cpu_brand + 0x10],eax
    mov [cpu_brand + 0x14],ebx
    mov [cpu_brand + 0x18],ecx
    mov [cpu_brand + 0x1c],edx

    mov eax,0x80000004
    cpuid
    mov [cpu_brand + 0x20],eax
    mov [cpu_brand + 0x24],ebx
    mov [cpu_brand + 0x28],ecx
    mov [cpu_brand + 0x2c],edx

return_point:                                ;用户程序返回点
    mov eax,core_data_seg_sel           ;使ds指向核心数据段
    mov ds,eax

    mov eax,core_stack_seg_sel          ;切换回内核自己的堆栈
    mov ss,eax 
    mov esp,[esp_pointer]

    mov ebx,message_6
    call sys_routine_seg_sel:put_string

    ;这里可以放置清除用户程序各种描述符的指令
    ;也可以加载并启动其它程序
       
    hlt
            
SECTION core_trail
core_end: